package com.raindrop.rd.parser;

import com.raindrop.rd.parser.token.Token;

/**
 * 记录源码相关信息，供{@link Token} 使用
 *
 * @author BillDowney
 * @date 2021/3/22 22:50
 */
public class Span {
    /**
     * 源码
     */
    private final String source;
    /**
     * 缓存字符串实例，以减少GC的压力
     */
    private final String cachedText;
    /**
     * 在源字符串中开始索引，从0开始
     */
    private final int start;
    /**
     * 源字符串的结束索引，从0开始
     */
    private final int end;
    /**
     * 当前行信息
     */
    private Line line;

    public Span(String source) {
        this(source, 0, source.length());
    }

    public Span(String source, int start, int end) {
        if (start > end) {
            throw new IllegalArgumentException("Start must be <= end.");
        }
        if (start < 0) {
            throw new IndexOutOfBoundsException("Start must be >= 0.");
        }
        if (start > source.length() - 1) {
            throw new IndexOutOfBoundsException("Start outside of string.");
        }
        if (end > source.length()) {
            throw new IndexOutOfBoundsException("End outside of string.");
        }

        this.source = source;
        this.start = start;
        this.end = end;
        this.cachedText = source.substring(start, end);
    }

    public Span(Span start, Span end) {
        if (!start.source.equals(end.source)) {
            throw new IllegalArgumentException("The two spans do not reference the same source.");
        }
        if (start.start > end.end) {
            throw new IllegalArgumentException("Start must be <= end.");
        }
        if (start.start < 0) {
            throw new IndexOutOfBoundsException("Start must be >= 0.");
        }
        if (start.start > start.source.length() - 1) {
            throw new IndexOutOfBoundsException("Start outside of string.");
        }
        if (end.end > start.source.length()) {
            throw new IndexOutOfBoundsException("End outside of string.");
        }
        this.source = start.source;
        this.start = start.start;
        this.end = end.end;
        this.cachedText = source.substring(this.start, this.end);
    }

    /**
     * 返回此span所引用的文本
     */
    public String getText() {
        return cachedText;
    }

    /**
     * 返回该区域第一个字符的索引
     */
    public int getStart() {
        return start;
    }

    /**
     * 返回这个空间的最后一个字符的索引加1
     */
    public int getEnd() {
        return end;
    }

    /**
     * 返回此span所引用的源字符串
     */
    public String getSource() {
        return source;
    }

    @Override
    public String toString() {
        return "Span [text=" + getText() + ", start=" + start + ", end=" + end + "]";
    }

    /**
     * 返回当前token所在的行。不能返回跨多行跨距的正确结果
     */
    public Line getLine() {
        if (this.line != null) {
            return this.line;
        }
        int lineStart = start;
        while (lineStart < end) {
            if (lineStart < 0) {
                break;
            }
            char c = source.charAt(lineStart);
            if (c == '\n') {
                lineStart = lineStart + 1;
                break;
            }
            lineStart--;
        }
        if (lineStart < 0) {
            lineStart = 0;
        }

        int lineEnd = end;
        while (true) {
            if (lineEnd > source.length() - 1) {
                break;
            }
            char c = source.charAt(lineEnd);
            if (c == '\n') {
                break;
            }
            lineEnd++;
        }

        int lineNumber = 0;
        int idx = lineStart;
        while (idx > 0 && idx < end) {
            char c = source.charAt(idx);
            if (c == '\n') {
                lineNumber++;
            }
            idx--;
        }
        lineNumber++;
        idx = lineStart + 1;
        int endLineNumber = lineNumber;
        while (idx < lineEnd) {
            char c = source.charAt(idx);
            if (c == '\n') {
                endLineNumber++;
            }
            idx++;
        }
        int startCol = this.start - lineStart + 1;
        int endCol = startCol + this.end - this.start - 1;
        this.line = new Line(source, lineStart, lineEnd, lineNumber, endLineNumber, startCol, endCol);
        return this.line;
    }

    public static class Line {
        private final String source;
        private final int start;
        private final int end;
        private final int lineNumber;
        private final int endLineNumber;
        private final int startCol;
        private final int endCol;

        public Line(String source, int start, int end, int lineNumber, int endLineNumber, int startCol, int endCol) {
            this.source = source;
            this.start = start;
            this.end = end;
            this.lineNumber = lineNumber;
            this.endLineNumber = endLineNumber;
            this.startCol = startCol;
            this.endCol = endCol;
        }

        public int getStartCol() {
            return startCol;
        }

        public int getEndCol() {
            return endCol;
        }

        public String getSource() {
            return source;
        }

        public int getStart() {
            return start;
        }

        public int getEnd() {
            return end;
        }

        public int getEndLineNumber() {
            return endLineNumber;
        }

        public int getLineNumber() {
            return lineNumber;
        }

        public String getText() {
            return source.substring(start, end);
        }
    }
}
